<?php
// @formatter:off
/**
 * @file WCSearch.php
 * @author Alejandro Dario Simi
 * @date $Date: 2013-08-20 00:23:06 +0000 (Tue, 20 Aug 2013) $
 *
 * $Id: WCSearch.php 102 2013-08-20 00:23:06Z daemonraco@gmail.com $
 * $URL: http://wcomix.googlecode.com/svn/tags/wcomix-1.0/includes/search/WCSearch.php $
 */
// @formatter:on

require_once dirname(__FILE__)."/WCSearchFactory.php";
require_once dirname(__FILE__)."/WCSearchContainer.php";

/**
 * @interface WCSearchIndexable This interface specifies basic method required
 * by any item that can be indexed by the search engine.
 */
interface WCSearchIndexable {
	/**
	 * Id of the item being indexed.
	 *
	 * @return int Returns an ID.
	 */
	public function id();
	/**
	 * This method allows to know whether an item is indexed or not. Also it
	 * allows to change such status.
	 *
	 * @param boolean $value New status to be set. When absent, this method
	 * works as a getter.
	 * @return boolean Returns true when the object is indexed.
	 */
	public function indexed($value = null);
	/**
	 * This method allows to get a list of words that must be associated with an item.
	 *
	 * @return string[] Returns a list of words.
	 */
	public function words();
}

/**
 * @class WCSearch This is the wcomix search engine. It holds all the required
 * logic to index and look for items inside a wcomix site.
 */
class WCSearch {
	/**
	 * @var string This is the identifier used for search caching.
	 */
	public static $CacheId = "SEARCH";
	/**
	 * @var WCSearch Pointer to the singleton instance.
	 */
	protected static $_Instance;
	/**
	 * @var WCConnection Database pointer shortcut.
	 */
	protected $_db;
	/**
	 * @var string Database tables' prefix shortcut.
	 */
	protected $_dbprefix;
	/**
	 * @var boolean This flag indicates if some basic statements are loaded.
	 */
	protected $_stmtLoaded = false;
	/**
	 * @var PDOStatement Statement to load exact words.
	 */
	protected $_stmtIndexSelect;
	/**
	 * @var PDOStatement Statement to insert new words.
	 */
	protected $_stmtIndexInsert;
	/**
	 * @var PDOStatement Statement to select exact word items for certain
	 * group.
	 */
	protected $_stmtItemSelect;
	/**
	 * @var PDOStatement Statement to load partial words.
	 */
	protected $_stmtIndexLike;
	/**
	 * @var int[string] List of all available words.
	 */
	protected $_loadedWords = array();
	/**
	 * @var WCTimer Pointer to the stopwatch.
	 */
	protected $_timer = null;
	/**
	 * Class constructor.
	 */
	protected function __construct() {
		//
		// Loading global pointers.
		global $wcDatabaseAccess;
		global $wcDefaults;
		//
		// Loading shortcuts.
		// @{
		$this->_db = WCConnection::Instance();
		$this->_dbprefix = $wcDatabaseAccess["prefix"];
		$this->_timer = WCTimer::Instance();
		// @}
	}
	/**
	 * Prevent users to clone the instance.
	 */
	public function __clone() {
		trigger_error(__CLASS__.": Clone is not allowed.", E_USER_ERROR);
	}
	//
	// Public methods.
	/**
	 * This method allows to know the elapsed time used to build results.
	 *
	 * @return int Returns an amount of milliseconds.
	 */
	public function buildTime() {
		//
		// Forwarding question to the stopwatch.
		return $this->timer("build");
	}
	/**
	 * This method allows to know the whole elapsed time used to search, find
	 * and generate results.
	 *
	 * @return int Returns an amount of milliseconds.
	 */
	public function elapsedTime() {
		//
		// Forwarding question to the stopwatch.
		return $this->timer("elapsed");
	}
	/**
	 * This method allows to know the elapsed time used to filter results.
	 *
	 * @return int Returns an amount of milliseconds.
	 */
	public function filteringTime() {
		//
		// Forwarding question to the stopwatch.
		return $this->timer("filter");
	}
	/**
	 * This method allows to get a list of items based on list of words to
	 * search.
	 *
	 * @oaram string[] $words List of words to look for.
	 * @oaram WCGroup $group Returned items must belong to this group.
	 * @oaram boolean $stats This flag indicates if a HTML commented summary
	 * must be prompted.
	 * @oaram string[] $onlyTypes List of allowed item types.
	 * @oaram string[] $exceptions List of words that must no be related to
	 * results.
	 */
	public function get(&$words = array(), WCGroup &$group, $stats = true, &$onlyTypes = array(), &$exceptions = array()) {
		//
		// Starting stopwatch for the whole elapsed time on this search.
		$this->startTimer("elapsed");
		//
		// Loading global pointers.
		global $wcPageCache;
		//
		// Clearing duplicated words.
		$words = array_keys(array_count_values($words));
		//
		// Creating a code to identify and cache this search.
		$code = base64_encode("[{$group->id}][".implode("|", $words)."][".implode("|", $exceptions)."]");
		//
		// Creating a container to hold resulting items.
		$results = new WCSearchContainer();
		//
		// Creating a pointer for a container to hold exception items.
		$eResults = null;
		//
		// Setting the list of words to results container.
		$results->setWords($words);
		//
		// Checking if there are exception.
		if($exceptions) {
			//
			// Creating a container for exception items.
			$eResults = new WCSearchContainer();
			//
			// Setting words to the container.
			$eResults->setWords($words);
		}
		//
		// Starting stopwatch for the elapsed time on seeking results.
		$this->startTimer("seek");
		//
		// Checking if this search was already made and it's cached.
		if(!$wcPageCache->get(self::$CacheId, false, $code)) {
			//
			// Loading basic statements.
			$this->loadStatements();
			//
			// Getting results with exact matches.
			$this->getItems($results, $group, $words, $this->_stmtIndexSelect, true);
			//
			// Getting results with partial matches.
			$this->getItems($results, $group, $words, $this->_stmtIndexLike, false);
			//
			// Triggering a review of results called 'second layer',
			// this adds sub items of current result as results.
			$this->secondLayer($results);
			//
			// Checking if there are exceptions.
			if($exceptions) {
				//
				// Starting stopwatch for the elapsed time on
				// filtering results.
				$this->startTimer("filter");
				//
				// Getting results with exact matches for
				// exceptions.
				$this->getItems($eResults, $group, $exceptions, $this->_stmtIndexSelect, true);
				//
				// Getting results with partial matches for
				// exceptions.
				$this->getItems($eResults, $group, $exceptions, $this->_stmtIndexLike, false);
				//
				// Triggering a review of exception results.
				$this->secondLayer($eResults);
				//
				// Filtering results.
				$results->filter($eResults);
				//
				// Stopping filtering results stopwatch.
				$this->stopTimer("filter");
			}
			//
			// Getting the list of resulting items.
			$result = $results->items();
			//
			// Adding search results to cache.
			$wcPageCache->append(self::$CacheId, serialize($result));
			//
			// Saving current cache configuration.
			$anyStatus = $wcPageCache->anyStatus();
			//
			// This search may be invalid for many situations.
			// @{
			$wcPageCache->anyCollection();
			$wcPageCache->anyComic();
			$wcPageCache->anyDirectory();
			$wcPageCache->anyTag();
			$wcPageCache->anyRule();
			// @}
			//
			// Storing search results on cache.
			$wcPageCache->save(self::$CacheId, $code);
			//
			// Restoring cache configuration.
			// @{
			$wcPageCache->anyCollection($anyStatus["collection"]);
			$wcPageCache->anyComic($anyStatus["comic"]);
			$wcPageCache->anyDirectory($anyStatus["directory"]);
			$wcPageCache->anyTag($anyStatus["tag"]);
			$wcPageCache->anyRule($anyStatus["rule"]);
			// @}
		} else {
			//
			// At this point, this is not the first time this search
			// is made and it's cached.
			//
			// Loading results from cache.
			$result = unserialize($wcPageCache->get(self::$CacheId, false, $code));
			//
			// Checking if stats must be shown or not.
			if($stats) {
				//
				// Loading cache information.
				$lastInfo = $wcPageCache->lastInfo();
				//
				// Prompting summary.
				// @{
				echo "<!--\n";
				echo "This is a cached search saved under code '".self::$CacheId."[{$code}]'\n\n";
				echo "ID:   {$lastInfo["id"]}\n";
				echo "CODE: {$lastInfo["code"]}\n\n";
				echo "COMPRESSED:   {$lastInfo["compressed"]}\n";
				echo "UNCOMPRESSED: {$lastInfo["uncompressed"]}\n";
				echo "RATE:         {$lastInfo["rate"]}%\n";
				echo "-->\n";
				// @}
			}
		}
		//
		// Stopping results seek time stopwatch.
		$this->stopTimer("seek");
		//
		// Starting stopwatch for the results building time.
		$this->startTimer("build");
		//
		// Creating a list to hold returning items.
		$items = array();
		//
		// Creating a flag to know f there are items to be filtered.
		$checkTypes = count($onlyTypes) > 0;
		//
		// Walking over all results an generating returning items.
		foreach($result as $hash => $info) {
			//
			// Checking if current item must be filtered based on its
			// type.
			if($checkTypes && !in_array($info["type"], $onlyTypes)) {
				continue;
			}
			//
			// Creating a returning item for current result.
			$item = WCSearchFactory::BuildItem($group, $info, $hash);
			//
			// If it's broken it must not be returned.
			if($item->isBroken()) {
				continue;
			}
			//
			// Adding new returning item to the list.
			$items[] = $item;
		}
		//
		// Stopping building results stopwatch.
		$this->stopTimer("build");
		//
		// Stopping whole elapsed time stopwatch.
		$this->stopTimer("elapsed");
		//
		// Returning findings.
		return $items;
	}
	/**
	 * This method allows to know the elapsed time on searching for results.
	 *
	 * @return int Returns an amount of milliseconds.
	 */
	public function searchTime() {
		return $this->timer("seek");
	}
	/**
	 * This method allows to know the elapsed time by the second layer review.
	 *
	 * @return int Returns an amount of milliseconds.
	 */
	public function secondLayerTime() {
		return $this->timer("second-layer");
	}
	/**
	 * This method allows to know a elapsed time of some criteria, by default,
	 * the whole elapsed time.
	 *
	 * @return int Returns an amount of milliseconds.
	 */
	public function timer($code = "elapsed") {
		return $this->_timer->timer($code);
	}
	/**
	 * This method allows to update the entire index. This means, if there's
	 * an item flagged as not indexed, it's added.
	 *
	 * @param boolean $showInfo This flag indicates if an inform text must be
	 * prompted.
	 * @param string $spacer This is a prefix for every inform line.
	 */
	public function updateIndex($showInfo = false, $spacer = "") {
		//
		// Loading global pointers.
		global $wcPageCache;
		//
		// Marking as invalid every cached search entry.
		$wcPageCache->resetFullId(self::$CacheId);
		//
		// Loading the full list of owrds.
		$this->loadWords();
		//
		// Removing obsolete index entries.
		$this->clearZombies($showInfo, $spacer);
		//
		// Updating comics index.
		$this->updateComicsIndex($showInfo, $spacer);
		//
		// Updating directories index.
		$this->updateDirectoriesIndex($showInfo, $spacer);
		//
		// Updating tags index.
		$this->updateTagsIndex($showInfo, $spacer);
		//
		// Updating collections index.
		$this->updateCollectionsIndex($showInfo, $spacer);
		//
		// Updating rules and actions index.
		$this->updateRulesIndex($showInfo, $spacer);
	}
	//
	// Protected methods.
	/**
	 * This method allows search for every entry on the index pointing to a
	 * dead end.
	 *
	 * @param boolean $showInfo This flag indicates if an inform text must be
	 * prompted.
	 * @param string $spacer This is a prefix for every inform line.
	 */
	protected function clearZombies($showInfo, $spacer = "") {
		//
		// Loading global pointers.
		global $wcDefaults;
		//
		// Checking that Read Only Mode is not activated.
		if(!$wcDefaults["read-only"]) {
			//
			// Creating a general statement id.
			$stSubID = __CLASS__."::".__FUNCTION__;
			//
			// Prompting action header.
			if($showInfo) {
				echo "{$spacer}Clearing Zombies:\n";
			}
			//
			// Creating a statement to remove dead pointers.
			// @{
			$stmtID = "{$stSubID}lostitems";
			if($this->_db->has($stmtID)) {
				$stmt = $this->_db->get($stmtID);
			} else {
				$query = "delete from  wc_search_items\n";
				$query .= "where        (sit_type = 'CX' and sit_comic      not in (select cmc_id from wc_comics where cmc_lost = 'N'))\n";
				$query .= " or          (sit_type = 'CL' and sit_collection not in (select col_id from wc_collections))\n";
				$query .= " or          (sit_type = 'TA' and sit_tag        not in (select tag_id from wc_tags))\n";
				$query .= " or          (sit_type = 'DI' and sit_directory  not in (select dir_id from wc_directories))\n";
				$stmt = $this->_db->prepare($stmtID, $query);
			}
			// @}
			//
			// Removing dead ends.
			$stmt->execute();
			//
			// Prompting action results.
			if($showInfo) {
				echo "{$spacer}\tNon existing items: {$stmt->rowCount()}\n";
			}
			//
			// Creating a statement to remove words with no items.
			// @{
			$stmtID = "{$stSubID}emptywords";
			if($this->_db->has($stmtID)) {
				$stmt = $this->_db->get($stmtID);
			} else {
				$query = "delete from wc_search_index\n";
				$query .= "where        six_id not in (\n";
				$query .= "                     select  sit_word\n";
				$query .= "                     from    wc_search_items)\n";
				$stmt = $this->_db->prepare($stmtID, $query);
			}
			// @}
			//
			// Removing empty words... how poetic :)
			$stmt->execute();
			//
			// Prompting action results.
			if($showInfo) {
				echo "{$spacer}\tWords without items: {$stmt->rowCount()}\n";
			}
			//
			// Creating a statement to remove items belonging to no
			// words.
			// @{
			$stmtID = "{$stSubID}lostwords";
			if($this->_db->has($stmtID)) {
				$stmt = $this->_db->get($stmtID);
			} else {
				$query = "delete from wc_search_items\n";
				$query .= "where        sit_word not in (\n";
				$query .= "                     select  six_id\n";
				$query .= "                     from    wc_search_index)\n";
				$stmt = $this->_db->prepare($stmtID, $query);
			}
			// @}
			//
			// Removing items of no words.
			$stmt->execute();
			//
			// Prompting action results.
			if($showInfo) {
				echo "{$spacer}\tItems of no word: {$stmt->rowCount()}\n";
			}
		}
	}
	/**
	 * This methods loads a list of items based on a statement and adds them
	 * to a search container.
	 *
	 * @param WCSearchContainer $results
	 * @param WCGroup $group
	 * @param string[] $words
	 * @param PDOStatement $stmt
	 * @param boolean $exact
	 */
	protected function getItems(WCSearchContainer &$results, WCGroup &$group, &$words, &$stmt, $exact = false) {
		//
		// Checking how many words are going to be added.
		$length = count($words);
		//
		// Searching results for each word.
		foreach($words as $word) {
			//
			// Requesting words from database.
			$stmt->execute(array(":word" => $word));
			//
			// Analyzing each words.
			foreach($stmt->fetchAll() as $row) {
				//
				// Requesting items from database based on a word.
				$this->_stmtItemSelect->execute(array(
					":word" => $row["six_id"],
					":group" => $group->id
				));
				//
				// Walking over found items.
				foreach($this->_stmtItemSelect->fetchAll() as $row2) {
					//
					// Adding items to the container based on
					// their types.
					switch($row2["sit_type"]) {
						case "CL":
							$results->addCollection($row2["sit_collection"], $group->id, $row["six_word"], $exact);
							break;
						case "CX":
							$results->addComic($row2["sit_comic"], $group->id, $row["six_word"], $exact);
							break;
						case "DI":
							$results->addDirectory($row2["sit_directory"], $group->id, $row["six_word"], $exact);
							break;
						case "RL":
							$results->addRule($row2["sit_rule"], $group->id, $row["six_word"], $exact);
							break;
						case "TA":
							$results->addTag($row2["sit_tag"], $group->id, $row["six_word"], $exact);
							break;
					}
				}
			}
		}
	}
	/**
	 * This method loads some statements required by this engine.
	 */
	protected function loadStatements() {
		//
		// Checking if these statements were already loaded.
		if(!$this->_stmtLoaded) {
			//
			// Generating a general statement id prefix.
			$stSubID = __CLASS__."::".__FUNCTION__;
			//
			// Creating a statement to load exact words.
			// @{
			$query = "select  *\n";
			$query .= "from    {$this->_dbprefix}search_index\n";
			$query .= "where   six_word = :word\n";
			$this->_stmtIndexSelect = $this->_db->prepare("{$stSubID}idxsel", $query);
			// @}
			//
			// Creating a statement to load partial words.
			// @{
			$query = "select  *\n";
			$query .= "from    {$this->_dbprefix}search_index\n";
			$query .= "where   six_word like concat(concat('%',:word),'%')\n";
			$query .= " and    six_word <> :word\n";
			$this->_stmtIndexLike = $this->_db->prepare("{$stSubID}idxlik", $query);
			// @}
			//
			// Creating a statement to insert new words.
			// @{
			$query = "insert ignore\n";
			$query .= "        into {$this->_dbprefix}search_index (six_word)\n";
			$query .= "        values (:word)\n";
			$this->_stmtIndexInsert = $this->_db->prepare("{$stSubID}idxins", $query);
			// @}
			//
			// Creating a statement to select exact word items for
			// certain group.
			// @{
			$query = "select  *\n";
			$query .= "from    {$this->_dbprefix}search_items\n";
			$query .= "where   sit_word  = :word\n";
			$query .= " and    sit_group = :group\n";
			$this->_stmtItemSelect = $this->_db->prepare("{$stSubID}itmsel", $query);
			// @}
			//
			// At this point, statements are considered to be loaded.
			$this->_stmtLoaded = true;
		}
	}
	/**
	 * This method loads the full list of available words.
	 */
	protected function loadWords() {
		//
		// Cleaning current list of words.
		$this->_loadedWords = array();
		//
		// Preparing a statement to fetch all words.
		// @{
		$stmt = false;
		$stmtID = __CLASS__."::".__FUNCTION__;
		if($this->_db->has($stmtID)) {
			$stmt = $this->_db->get($stmtID);
		} else {
			$query = "select  *\n";
			$query .= "from    {$this->_dbprefix}search_index\n";
			$stmt = $this->_db->prepare($stmtID, $query);
		}
		// @}
		//
		// Asking words.
		$stmt->execute();
		//
		// Fetching and loading every word.
		foreach($stmt->fetchAll() as $word) {
			$this->_loadedWords[$word["six_word"]] = $word["six_id"];
		}
	}
	/**
	 * This method performs a second review on each item adding its sub-items
	 * as valid results.
	 *
	 * @param WCSearchContainer $results This container is the one to use as
	 * list of items and container for sub-items. At the end, this container
	 * will hold original items and second layer items.
	 */
	protected function secondLayer(WCSearchContainer &$results) {
		//
		// Starting stopwatch for the second layer review time.
		$this->startTimer("second-layer");
		//
		// Creating an array to hold second layer item IDs ordered by the
		// type.
		$secondLayer = array();
		//
		// Even if there were no results for one of these types, the empty
		// list is built to avoid dynamic creation and gain performance in
		// the amount of instructions and array accesses.
		foreach(WCSearchFactory::$TypePriority as $type => $value) {
			$secondLayer[$type] = array();
		}
		//
		// Creating a temporary list of results.
		$entries = array();
		//
		// Filling id lists with current values.
		foreach(WCSearchFactory::$TypePriority as $type => $value) {
			$secondLayer[$type] = $results->typeIds($type);
		}
		//
		// Creating a group's id shortcut.
		$gid = $results->gid();
		//
		// Searching for second-layer tag items.
		if(isset($secondLayer["TA"][0])) {
			//
			// Creating a statement to load tag members.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__."[TA]";
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "select  *\n";
				$query .= "from    {$this->_dbprefix}tag_members\n";
				$query .= "where   mem_group = :group\n";
				$query .= " and    mem_tag   = :tag\n";

				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Building database parameters.
			$params = array(
				":group" => $gid,
				":tag" => 0
			);
			//
			// Walking over first layer tags.
			foreach($secondLayer["TA"] as $tag) {
				//
				// Setting current first layer tag on database
				// parameters.
				$params[":tag"] = $tag;
				//
				// Requesting second layer items.
				if($stmt->execute($params)) {
					//
					// Creating a parent shortcut.
					$parent = $results->item("TA", $tag, $gid);
					//
					// Adding each member.
					foreach($stmt->fetchAll() as $row) {
						$results->addComic($row["mem_comic"], $gid, false, false, $parent);
					}
				}
			}
		}
		//
		// Searching for second-layer directory items.
		if(isset($secondLayer["DI"][0])) {
			//
			// Creating a statement to load directory members.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__."[DI]";
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "select  *\n";
				$query .= "from    {$this->_dbprefix}comics\n";
				$query .= "where   cmc_group     = :group\n";
				$query .= " and    cmc_directory = :directory\n";

				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Building database parameters.
			$params = array(
				":group" => $gid,
				":directory" => 0
			);
			//
			// Walking over first layer directories.
			foreach($secondLayer["DI"] as $directory) {
				//
				// Setting current first layer directory on
				// database parameters.
				$params[":directory"] = $directory;
				//
				// Requesting second layer items.
				if($stmt->execute($params)) {
					//
					// Creating a parent shortcut.
					$parent = $results->item("DI", $directory, $gid);
					//
					// Adding each member.
					foreach($stmt->fetchAll() as $row) {
						$results->addComic($row["cmc_id"], $gid, false, false, $parent);
					}
				}
			}
		}
		//
		// Searching for second-layer collection items.
		if(isset($secondLayer["CL"][0])) {
			//
			// Creating a statement to load collection's comics.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__."[CL+CX]";
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "select  *\n";
				$query .= "from    {$this->_dbprefix}comics\n";
				$query .= "where   cmc_group      = :group\n";
				$query .= " and    cmc_collection = :collection\n";

				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Building database parameters.
			$params = array(
				":group" => $gid,
				":collection" => 0
			);
			//
			// Walking over first layer collections.
			foreach($secondLayer["CL"] as $collection) {
				//
				// Setting current first layer collection on
				// database parameters.
				$params[":collection"] = $collection;
				//
				// Requesting second layer items.
				if($stmt->execute($params)) {
					//
					// Creating a parent shortcut.
					$parent = $results->item("CL", $collection, $gid);
					//
					// Adding each comic member.
					foreach($stmt->fetchAll() as $row) {
						$results->addComic($row["cmc_id"], $gid, false, false, $parent);
					}
				}
			}
			//
			// Creating a statement to load collection's collections.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__."[CL+CL]";
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "select  *\n";
				$query .= "from    {$this->_dbprefix}collections\n";
				$query .= "where   col_group  = :group\n";
				$query .= " and    col_parent = :collection\n";

				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Building database parameters.
			$params = array(
				":group" => $gid,
				":collection" => 0
			);
			//
			// Walking over first layer tags.
			foreach($secondLayer["CL"] as $collection) {
				//
				// Setting current first layer collection on
				// database parameters.
				$params[":collection"] = $collection;
				//
				// Requesting second layer items.
				if($stmt->execute($params)) {
					//
					// Creating a parent shortcut.
					$parent = $results->item("CL", $collection, $gid);
					//
					// Adding each member.
					foreach($stmt->fetchAll() as $row) {
						$results->addCollection($row["col_id"], $gid, false, false, $parent);
					}
				}
			}
		}
		//
		// Searching for second-layer comic items.
		if(isset($secondLayer["CX"][0])) {
			//
			// Creating a statement to load comics' directories and
			// collections.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__."[CX]";
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "select  *\n";
				$query .= "from    {$this->_dbprefix}comics\n";
				$query .= "where   cmc_group = :group\n";
				$query .= " and    cmc_id    = :comic\n";

				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Building database parameters.
			$params = array(
				":group" => $gid,
				":comic" => 0
			);
			//
			// Walking over first layer comics.
			foreach($secondLayer["CX"] as $comic) {
				//
				// Setting current first layer comic on database
				// parameters.
				$params[":comic"] = $comic;
				//
				// Requesting second layer items.
				if($stmt->execute($params)) {
					//
					// Creating a parent shortcut.
					$parent = $results->item("CX", $comic, $gid);
					//
					// Adding each member.
					foreach($stmt->fetchAll() as $row) {
						//
						// Adding comic directory.
						$results->addDirectory($row["cmc_directory"], $gid, false, false, $parent);
						//
						// Adding comic collections, when it has one.
						if($row["cmc_collection"]) {
							$results->addCollection($row["cmc_collection"], $gid, false, false, $parent);
						}
					}
				}
			}
			//
			// Creating a statement to load comics' tags.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__."[CX+TA]";
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "select  *\n";
				$query .= "from    {$this->_dbprefix}tag_members\n";
				$query .= "where   mem_group = :group\n";
				$query .= " and    mem_comic = :comic\n";

				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Building database parameters.
			$params = array(
				":group" => $gid,
				":comic" => 0
			);
			//
			// Walking over first layer comics.
			foreach($secondLayer["CX"] as $comic) {
				//
				// Setting current first layer comic on database
				// parameters.
				$params[":comic"] = $comic;
				//
				// Requesting second layer items.
				if($stmt->execute($params)) {
					//
					// Creating a parent shortcut.
					$parent = $results->item("CX", $comic, $gid);
					//
					// Adding each member.
					foreach($stmt->fetchAll() as $row) {
						$results->addTag($row["mem_tag"], $gid, false, false, $parent);
					}
				}
			}
		}
		//
		// Stopping second layer review time stopwatch.
		$this->stopTimer("second-layer");
	}
	/**
	 * This method allows to start the stopwatch for certain measurement.
	 *
	 * @param string $code Measurement id.
	 */
	protected function startTimer($code) {
		$this->_timer->start($code);
	}
	/**
	 * This method allows to stop the stopwatch of certain measurement.
	 *
	 * @param string $code Measurement id.
	 */
	protected function stopTimer($code) {
		$this->_timer->stop($code);
	}
	/**
	 * This method allows to update collections' index. This means, if there's
	 * a collection flagged as not indexed, it's added.
	 *
	 * @param boolean $showInfo This flag indicates if an inform text must be
	 * prompted.
	 * @param string $spacer This is a prefix for every inform line.
	 */
	protected function updateCollectionsIndex($showInfo, $spacer = "") {
		//
		// Loading global pointers.
		global $wcDefaults;
		//
		// Checking that Read Only Mode is not activated.
		if(!$wcDefaults["read-only"]) {
			//
			// Loading basic statements.
			$this->loadStatements();
			//
			// Creating a general statement id.
			$stSubID = __CLASS__."::".__FUNCTION__;
			//
			// Creating required statements.
			// @{
			$stmtID = "{$stSubID}colsel";
			if($this->_db->has($stmtID)) {
				$stmtColSel = $this->_db->get($stmtID);
			} else {
				$query = "select  *\n";
				$query .= "from    {$this->_dbprefix}collections\n";
				$query .= "where   col_indexed = 'N'\n";
				$stmtColSel = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}colmrk";
			if($this->_db->has($stmtID)) {
				$stmtColMrk = $this->_db->get($stmtID);
			} else {
				$query = "update  {$this->_dbprefix}collections\n";
				$query .= "set     col_indexed = 'Y'\n";
				$query .= "where   col_id    = :id\n";
				$query .= " and    col_group = :group\n";
				$stmtColMrk = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}colins";
			if($this->_db->has($stmtID)) {
				$stmtColIns = $this->_db->get($stmtID);
			} else {
				$query = "insert ignore\n";
				$query .= "        into {$this->_dbprefix}search_items (sit_word, sit_order, sit_type, sit_group, sit_directory, sit_comic, sit_tag, sit_collection, sit_rule)\n";
				$query .= "        values (:id, :order, 'CL', :group, null, null, null, :collection, null)\n";
				$stmtColIns = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}colclr";
			if($this->_db->has($stmtID)) {
				$stmtColClr = $this->_db->get($stmtID);
			} else {
				$query = "delete from     {$this->_dbprefix}search_items\n";
				$query .= "where           sit_collection = :id\n";
				$query .= " and            sit_group      = :group\n";
				$stmtColClr = $this->_db->prepare($stmtID, $query);
			}
			// @}
			//
			// Loading not indexed collections.
			$stmtColSel->execute();
			//
			// Walking over each collection and adding it to index.
			foreach($stmtColSel->fetchAll() as $row) {
				//
				// Removing current collection indexed
				// information, if there's any.
				$stmtColClr->execute(array(
					":id" => $row["col_id"],
					":group" => $row["col_group"]
				));
				//
				// Creating a list of words related with this
				// collections, based on it's name.
				$words = WCSearchFactory::StringToPatterns($row["col_name"]);
				//
				// Prompting action.
				if($showInfo) {
					echo "{$spacer}Adding collection '{$row["col_name"]}' ({$row["col_id"]})\n";
				}
				//
				// Creating a counter to known each collection word
				// order.
				$order = 1;
				//
				// Analyzing each collection words and creating
				// index entries for them.
				foreach($words as $order => $word) {
					//
					// When current word is not cached, it
					// means it's not in the database yet.

					if(!isset($this->_loadedWords[$word])) {
						//
						// Building database parameters.
						$param = array(":word" => $word);
						//
						// Adding new word.
						$this->_stmtIndexInsert->execute($param);
						//
						// Fetching new word.
						$this->_stmtIndexSelect->execute($param);
						//
						// Fetching new word data.
						$row2 = $this->_stmtIndexSelect->fetch();
						//
						// Adding word to cache.
						$this->_loadedWords[$word] = $row2["six_id"];
					}
					//
					// Adding collections entry for current word.
					$stmtColIns->execute(array(
						":id" => $this->_loadedWords[$word],
						":order" => $order + 1,
						":group" => $row["col_group"],
						":collection" => $row["col_id"]
					));
				}
				//
				// Marking current collection as indexed.
				$stmtColMrk->execute(array(
					":id" => $row["col_id"],
					":group" => $row["col_group"]
				));
			}
		}
	}
	/**
	 * This method allows to update comics' index. This means, if there's a
	 * comic flagged as not indexed, it's added.
	 *
	 * @param boolean $showInfo This flag indicates if an inform text must be
	 * prompted.
	 * @param string $spacer This is a prefix for every inform line.
	 */
	protected function updateComicsIndex($showInfo, $spacer = "") {
		//
		// Loading global pointers.
		global $wcDefaults;
		//
		// Checking that Read Only Mode is not activated.
		if(!$wcDefaults["read-only"]) {
			//
			// Loading basic statements.
			$this->loadStatements();
			//
			// Creating a general statement id.
			$stSubID = __CLASS__."::".__FUNCTION__;
			//
			// Creating required statements.
			// @{
			$stmtID = "{$stSubID}cmcsel";
			if($this->_db->has($stmtID)) {
				$stmtCmcSel = $this->_db->get($stmtID);
			} else {
				$query = "select  *\n";
				$query .= "from    {$this->_dbprefix}comics\n";
				$query .= "where   cmc_lost    = 'N'\n";
				$query .= " and    cmc_indexed = 'N'\n";
				$stmtCmcSel = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}cmcmrk";
			if($this->_db->has($stmtID)) {
				$stmtCmcMrk = $this->_db->get($stmtID);
			} else {
				$query = "update  {$this->_dbprefix}comics\n";
				$query .= "set     cmc_indexed = 'Y'\n";
				$query .= "where   cmc_id    = :id\n";
				$query .= " and    cmc_group = :group\n";
				$stmtCmcMrk = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}cmcins";
			if($this->_db->has($stmtID)) {
				$stmtCmcIns = $this->_db->get($stmtID);
			} else {
				$query = "insert ignore\n";
				$query .= "        into {$this->_dbprefix}search_items (sit_word, sit_order, sit_type, sit_group, sit_directory, sit_comic, sit_tag, sit_collection, sit_rule)\n";
				$query .= "        values (:id, :order, 'CX', :group, null, :comic, null, null, null)\n";
				$stmtCmcIns = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}cmcclr";
			if($this->_db->has($stmtID)) {
				$stmtCmcClr = $this->_db->get($stmtID);
			} else {
				$query = "delete from     {$this->_dbprefix}search_items\n";
				$query .= "where           sit_comic = :id\n";
				$query .= " and            sit_group = :group\n";
				$stmtCmcClr = $this->_db->prepare($stmtID, $query);
			}
			// @}
			//
			// Loading not indexed comics.
			$stmtCmcSel->execute();
			//
			// Walking over each comic and adding it to index.
			foreach($stmtCmcSel->fetchAll() as $row) {
				//
				// Removing current comic indexed information, if
				// there's any.
				$stmtCmcClr->execute(array(
					":id" => $row["cmc_id"],
					":group" => $row["cmc_group"]
				));
				//
				// Creating a list of words related with this
				// comics, based on it's name.
				$words = WCSearchFactory::StringToPatterns($row["cmc_next_name"] ? $row["cmc_next_name"] : $row["cmc_name"]);
				//
				// Prompting action.
				if($showInfo) {
					echo "{$spacer}Adding comic '{$row["cmc_name"]}' ({$row["cmc_id"]}@{$row["cmc_directory"]})\n";
				}
				//
				// Creating a counter to known each comic word
				// order.
				$order = 1;
				//
				// Analyzing each comic words and creating index
				// entries for them.
				foreach($words as $order => $word) {
					//
					// When current word is not cached, it
					// means it's not in the database yet.
					if(!isset($this->_loadedWords[$word])) {
						//
						// Building database parameters.
						$param = array(":word" => $word);
						//
						// Adding new word.
						$this->_stmtIndexInsert->execute($param);
						//
						// Fetching new word.
						$this->_stmtIndexSelect->execute($param);
						//
						// Fetching new word data.
						$row2 = $this->_stmtIndexSelect->fetch();
						//
						// Adding word to cache.
						$this->_loadedWords[$word] = $row2["six_id"];
					}
					//
					// Adding comics entry for current word.
					$stmtCmcIns->execute(array(
						":id" => $this->_loadedWords[$word],
						":order" => $order + 1,
						":group" => $row["cmc_group"],
						":comic" => $row["cmc_id"]
					));
				}
				//
				// Marking current comic as indexed.
				$stmtCmcMrk->execute(array(
					":id" => $row["cmc_id"],
					":group" => $row["cmc_group"]
				));
			}
		}
	}
	/**
	 * This method allows to update directories' index. This means, if there's
	 * a directory flagged as not indexed, it's added.
	 *
	 * @param boolean $showInfo This flag indicates if an inform text must be
	 * prompted.
	 * @param string $spacer This is a prefix for every inform line.
	 */
	protected function updateDirectoriesIndex($showInfo, $spacer = "") {
		//
		// Loading global pointers.
		global $wcDefaults;
		//
		// Checking that Read Only Mode is not activated.
		if(!$wcDefaults["read-only"]) {
			//
			// Loading basic statements.
			$this->loadStatements();
			//
			// Creating a general statement id.
			$stSubID = __CLASS__."::".__FUNCTION__;
			//
			// Creating required statements.
			// @{
			$stmtID = "{$stSubID}dirsel";
			if($this->_db->has($stmtID)) {
				$stmtDirSel = $this->_db->get($stmtID);
			} else {
				$query = "select  *\n";
				$query .= "from    {$this->_dbprefix}directories\n";
				$query .= "where   dir_indexed = 'N'\n";
				$stmtDirSel = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}dirmrk";
			if($this->_db->has($stmtID)) {
				$stmtDirMrk = $this->_db->get($stmtID);
			} else {
				$query = "update  {$this->_dbprefix}directories\n";
				$query .= "set     dir_indexed = 'Y'\n";
				$query .= "where   dir_id    = :id\n";
				$query .= " and    dir_group = :group\n";
				$stmtDirMrk = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}dirins";
			if($this->_db->has($stmtID)) {
				$stmtDirIns = $this->_db->get($stmtID);
			} else {
				$query = "insert ignore\n";
				$query .= "        into {$this->_dbprefix}search_items (sit_word, sit_order, sit_type, sit_group, sit_directory, sit_comic, sit_tag, sit_collection, sit_rule)\n";
				$query .= "        values (:id, :order, 'DI', :group, :directory, null, null, null, null)\n";
				$stmtDirIns = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}dirclr";
			if($this->_db->has($stmtID)) {
				$stmtDirClr = $this->_db->get($stmtID);
			} else {
				$query = "delete from     {$this->_dbprefix}search_items\n";
				$query .= "where           sit_directory = :id\n";
				$query .= " and            sit_group     = :group\n";
				$stmtDirClr = $this->_db->prepare($stmtID, $query);
			}
			// @}
			//
			// Loading not indexed directories.
			$stmtDirSel->execute();
			//
			// Walking over each directory and adding it to index.
			foreach($stmtDirSel->fetchAll() as $row) {
				//
				// Removing current directory indexed information,
				// if there's any.
				$stmtDirClr->execute(array(
					":id" => $row["dir_id"],
					":group" => $row["dir_group"]
				));
				//
				// Creating a list of words related with this
				// directories, based on it's name.
				$words = WCSearchFactory::StringToPatterns($row["dir_next_name"] ? $row["dir_next_name"] : $row["dir_name"]);
				//
				// Prompting action.
				if($showInfo) {
					echo "{$spacer}Adding directory '{$row["dir_name"]}' ({$row["dir_id"]})\n";
				}
				//
				// Creating a counter to known each directory word
				// order.
				$order = 1;
				//
				// Analyzing each directory words and creating index
				// entries for them.
				foreach($words as $order => $word) {
					//
					// When current word is not cached, it
					// means it's not in the database yet.
					if(!isset($this->_loadedWords[$word])) {
						//
						// Building database parameters.
						$param = array(":word" => $word);
						//
						// Adding new word.
						$this->_stmtIndexInsert->execute($param);
						//
						// Fetching new word.
						$this->_stmtIndexSelect->execute($param);
						//
						// Fetching new word data.
						$row2 = $this->_stmtIndexSelect->fetch();
						//
						// Adding word to cache.
						$this->_loadedWords[$word] = $row2["six_id"];
					}
					//
					// Adding directories entry for current
					// word.
					$stmtDirIns->execute(array(
						":id" => $this->_loadedWords[$word],
						":order" => $order + 1,
						":group" => $row["dir_group"],
						":directory" => $row["dir_id"]
					));
				}
				//
				// Marking current directory as indexed.
				$stmtDirMrk->execute(array(
					":id" => $row["dir_id"],
					":group" => $row["dir_group"]
				));
			}
		}
	}
	/**
	 * This method allows to update rules' index. This means, if there's a
	 * rule flagged as not indexed, it's added.
	 *
	 * @param boolean $showInfo This flag indicates if an inform text must be
	 * prompted.
	 * @param string $spacer This is a prefix for every inform line.
	 */
	protected function updateRulesIndex($showInfo, $spacer = "") {
		//
		// Loading global pointers.
		global $wcDefaults;
		//
		// Checking that Read Only Mode is not activated.
		if(!$wcDefaults["read-only"]) {
			//
			// Loading basic statements.
			$this->loadStatements();
			//
			// Creating a general statement id.
			$stSubID = __CLASS__."::".__FUNCTION__;
			//
			// Creating required statements.
			// @{
			$stmtID = "{$stSubID}rulsel";
			if($this->_db->has($stmtID)) {
				$stmtRuleSel = $this->_db->get($stmtID);
			} else {
				$query = "select  *\n";
				$query .= "from    {$this->_dbprefix}rules\n";
				$query .= "where   rul_indexed = 'N'\n";
				$stmtRuleSel = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}ractsel";
			if($this->_db->has($stmtID)) {
				$stmtRActSel = $this->_db->get($stmtID);
			} else {
				$query = "select  *\n";
				$query .= "from    {$this->_dbprefix}rule_actions\n";
				$query .= "where   rel_ra_indexed = 'N'\n";
				$stmtRActSel = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}rulins";
			if($this->_db->has($stmtID)) {
				$stmtRuleIns = $this->_db->get($stmtID);
			} else {
				$query = "insert ignore\n";
				$query .= "        into {$this->_dbprefix}search_items (sit_word, sit_order, sit_type, sit_group, sit_directory, sit_comic, sit_tag, sit_collection, sit_rule)\n";
				$query .= "        values (:id, :order, 'RL', :group, null, null, null, null, :rule)\n";
				$stmtRuleIns = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}rulclr";
			if($this->_db->has($stmtID)) {
				$stmtRuleClr = $this->_db->get($stmtID);
			} else {
				$query = "delete from     {$this->_dbprefix}search_items\n";
				$query .= "where           sit_rule  = :id\n";
				$query .= " and            sit_group = :group\n";
				$stmtRuleClr = $this->_db->prepare($stmtID, $query);
			}
			// @}
			//
			// Loading not indexed rules and actions.
			$stmtRuleSel->execute();
			$stmtRActSel->execute();
			//
			// Checking if there's any action or rule not indexed.
			if($stmtRuleSel->rowCount() > 0 || $stmtRActSel->rowCount() > 0) {
				//
				// Loading global pointers.
				global $wcGroupsHolder;
				global $wcRulesHolder;
				//
				// Walking over each available group.
				foreach($wcGroupsHolder->groups() as $group) {
					//
					// Walking over each available rule in
					// current group.
					foreach($wcRulesHolder->items($group) as $rule) {
						//
						// If current rule is already
						// index it gets ignored.
						if($rule->indexed()) {
							continue;
						}
						//
						// Removing current rule indexed
						// information, if there's any.
						$stmtRuleClr->execute(array(
							":id" => $rule->id,
							":group" => $group->id
						));
						//
						// Creating a list of words
						// related with this rule's name
						// and its actions' names.
						$words = $rule->words();
						//
						// Prompting action.
						if($showInfo) {
							echo "{$spacer}Adding rule '{$rule->name}' ({$rule->id})\n";
						}
						//
						// Creating a counter to known
						// each rule word order.
						$order = 1;
						//
						// Analyzing each rule words and
						// creating index entries for
						// them.
						foreach($words as $order => $word) {
							//
							// When current word is
							// not cached, it means
							// it's not in the
							// database yet.
							if(!isset($this->_loadedWords[$word])) {///@todo ver si entra en estos IF
								//
								// Building database parameters.
								$param = array(":word" => $word);
								//
								// Adding new word.
								$this->_stmtIndexInsert->execute($param);
								//
								// Fetching new word.
								$this->_stmtIndexSelect->execute($param);
								//
								// Fetching new word data.
								$row2 = $this->_stmtIndexSelect->fetch();
								//
								// Adding word to cache.
								$this->_loadedWords[$word] = $row2["six_id"];
							}
							//
							// Adding rules entry for
							// current word.
							$stmtRuleIns->execute(array(
								":id" => $this->_loadedWords[$word],
								":order" => $order + 1,
								":group" => $group->id,
								":rule" => $rule->id
							));
						}
						//
						// Marking current rule as indexed.
						$rule->indexed(true);
					}
				}
			}
		}
	}
	/**
	 * This method allows to update tags' index. This means, if there's a
	 * tag flagged as not indexed, it's added.
	 *
	 * @param boolean $showInfo This flag indicates if an inform text must be
	 * prompted.
	 * @param string $spacer This is a prefix for every inform line.
	 */
	protected function updateTagsIndex($showInfo, $spacer = "") {
		//
		// Loading global pointers.
		global $wcDefaults;
		//
		// Checking that Read Only Mode is not activated.
		if(!$wcDefaults["read-only"]) {
			//
			// Loading basic statements.
			$this->loadStatements();
			//
			// Creating a general statement id.
			$stSubID = __CLASS__."::".__FUNCTION__;
			//
			// Creating required statements.
			// @{
			$stmtID = "{$stSubID}tagsel";
			if($this->_db->has($stmtID)) {
				$stmtTagSel = $this->_db->get($stmtID);
			} else {
				$query = "select  *\n";
				$query .= "from    {$this->_dbprefix}tags\n";
				$query .= "where   tag_indexed = 'N'\n";
				$stmtTagSel = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}tagmrk";
			if($this->_db->has($stmtID)) {
				$stmtTagMrk = $this->_db->get($stmtID);
			} else {
				$query = "update  {$this->_dbprefix}tags\n";
				$query .= "set     tag_indexed = 'Y'\n";
				$query .= "where   tag_id    = :id\n";
				$query .= " and    tag_group = :group\n";
				$stmtTagMrk = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}tagins";
			if($this->_db->has($stmtID)) {
				$stmtTagIns = $this->_db->get($stmtID);
			} else {
				$query = "insert ignore\n";
				$query .= "        into {$this->_dbprefix}search_items (sit_word, sit_order, sit_type, sit_group, sit_directory, sit_comic, sit_tag, sit_collection, sit_rule)\n";
				$query .= "        values (:id, :order, 'TA', :group, null, null, :tag, null, null)\n";
				$stmtTagIns = $this->_db->prepare($stmtID, $query);
			}

			$stmtID = "{$stSubID}tagclr";
			if($this->_db->has($stmtID)) {
				$stmtTagClr = $this->_db->get($stmtID);
			} else {
				$query = "delete from     {$this->_dbprefix}search_items\n";
				$query .= "where           sit_tag   = :id\n";
				$query .= " and            sit_group = :group\n";
				$stmtTagClr = $this->_db->prepare($stmtID, $query);
			}
			// @}
			//
			// Loading not indexed tags.
			$stmtTagSel->execute();
			//
			// Walking over each tag and adding it to index.
			foreach($stmtTagSel->fetchAll() as $row) {
				//
				// Removing current tag indexed information, if
				// there's any.
				$stmtTagClr->execute(array(
					":id" => $row["tag_id"],
					":group" => $row["tag_group"]
				));
				//
				// Creating a list of words related with this
				// tags, based on it's name.
				$words = WCSearchFactory::StringToPatterns($row["tag_name"]);
				//
				// Prompting action.
				if($showInfo) {
					echo "{$spacer}Adding tag '{$row["tag_name"]}' ({$row["tag_id"]})\n";
				}
				//
				// Creating a counter to known each tag word
				// order.
				$order = 1;
				//
				// Analyzing each tag words and creating index
				// entries for them.
				foreach($words as $order => $word) {
					//
					// When current word is not cached, it
					// means it's not in the database yet.
					if(!isset($this->_loadedWords[$word])) {
						//
						// Building database parameters.
						$param = array(":word" => $word);
						//
						// Adding new word.
						$this->_stmtIndexInsert->execute($param);
						//
						// Fetching new word.
						$this->_stmtIndexSelect->execute($param);
						//
						// Fetching new word data.
						$row2 = $this->_stmtIndexSelect->fetch();
						//
						// Adding word to cache.
						$this->_loadedWords[$word] = $row2["six_id"];
					}
					//
					// Adding tags entry for current word.
					$stmtTagIns->execute(array(
						":id" => $this->_loadedWords[$word],
						":order" => $order + 1,
						":group" => $row["tag_group"],
						":tag" => $row["tag_id"]
					));
				}
				//
				// Marking current tag as indexed.
				$stmtTagMrk->execute(array(
					":id" => $row["tag_id"],
					":group" => $row["tag_group"]
				));
			}
		}
	}
	//
	// Public class methods.
	/**
	 * This class method allows to get the sole instance of this engine.
	 *
	 * @return WCSearch Returns the singleton instance of this class.
	 */
	public static function Instance() {
		//
		// Checking if there's no instance yet.
		if(!isset(self::$_Instance)) {
			$c = __CLASS__;
			//
			// Creating the sole instance.
			self::$_Instance = new $c;
		}
		//
		// Returning the class instance.
		return self::$_Instance;
	}
}
?>
